import { makeLegalIdentifier, normalizePath } from '@rollup/pluginutils'
import fs, { outputFile } from 'fs-extra'
import isGlob from 'is-glob'
import { default as nodePath, default as path } from 'path'
import picomatch from 'picomatch'
import { CWD } from './constances'
import {
  BaseResolvedGlobConfig,
  GlobConfig,
  Loader,
  ReactRouteRaw,
  UserOptions,
  VueRouteRaw,
} from './types'
import {
  countSlash,
  createAnyValueStringify,
  extensions,
  getHashedIdentifier,
  getUniqueIdentifier,
  isCatchAllRoute,
  isDynamicRoute,
  mergeTransformHook,
  removeExtension,
  toArray,
  uniqueString,
  urlSuffix,
} from './utils'
import { GlobImportWatch, globImportWatch } from './watch'

const getAbsModulePath = (p: string) => path.normalize(path.resolve(CWD, p))

const NoExported = (filename: string, dynamicImp: boolean | 'async') => {
  return dynamicImp
    ? `// @ts-ignore\nimport(${JSON.stringify(filename)});`
    : `// @ts-ignore\nimport ${JSON.stringify(filename)};`
}

const exportedDefault = (filename: string, id: string, dynamicImp: boolean | 'async') => {
  return dynamicImp === 'async'
    ? `export const ${id} =// @ts-ignore\n /*#__PURE__*/ async () => { const _ = await import(${JSON.stringify(
        filename
      )}); return (_ && typeof _ === 'object' && 'default' in _) ? _.default : _; };`
    : dynamicImp
    ? `export const ${id} =// @ts-ignore\n /*#__PURE__*/ import(${JSON.stringify(
        filename
      )}).then(_ => (_ && typeof _ === 'object' && 'default' in _) ? _.default : _)`
    : `// @ts-ignore\nimport ${id} from ${JSON.stringify(filename)};// @ts-ignore\nexport { ${id} }`
}

const exportedNamedModule = (filename: string, id: string, dynamicImp: boolean | 'async') => {
  return dynamicImp === 'async'
    ? `export const ${id} =// @ts-ignore\n /*#__PURE__*/ () => import(${JSON.stringify(filename)});`
    : dynamicImp
    ? `export const ${id} =// @ts-ignore\n /*#__PURE__*/ import(${JSON.stringify(filename)});`
    : `// @ts-ignore\nimport * as ${id} from ${JSON.stringify(
        filename
      )};// @ts-ignore\nexport { ${id} }`
}

const RawRouteKey = Symbol()
const RawComponentKey = Symbol()
const ImportModeKey = Symbol()
const stringify = createAnyValueStringify()

const defaultloaders: Record<string, Loader> = {
  *import(files, config) {
    const suffix = urlSuffix(config.hash, config.query)
    for (const file of files) {
      yield NoExported(getAbsModulePath(file) + suffix, config.dynamicImport(file))
    }
  },
  *default(files, config) {
    const suffix = urlSuffix(config.hash, config.query)
    const _default: Record<string, string> = {}
    const used_ids = new Set<string>()
    for (const file of files) {
      const filename = getAbsModulePath(file)
      const exportedId = uniqueString(config.moduleIdRenamer(filename, config), used_ids)
      const dynamicImp = config.dynamicImport(file)

      yield exportedDefault(filename + suffix, exportedId, dynamicImp)
      _default[config.moduleKeyRenamer(filename, config)] = exportedId
    }

    yield `// @ts-ignore\nexport default /*#__PURE__*/ { ${Object.entries(_default)
      .map(([key, id]) => `${JSON.stringify(key)}: ${id}`)
      .join(', ')} }`
  },
  *named(files, config) {
    const suffix = urlSuffix(config.hash, config.query)
    for (const file of files) {
      const filename = getAbsModulePath(file)
      yield `// @ts-ignore\nexport * from ${JSON.stringify(filename + suffix)};`
    }
  },
  *namedModule(files, config) {
    const suffix = urlSuffix(config.hash, config.query)
    const _default: Record<string, string> = {}
    const used_ids = new Set<string>()
    for (const file of files) {
      const filename = getAbsModulePath(file)
      const exportedId = uniqueString(config.moduleIdRenamer(filename, config), used_ids)
      const dynamicImp = config.dynamicImport(file)

      yield exportedNamedModule(filename + suffix, exportedId, dynamicImp)
      _default[config.moduleKeyRenamer(filename, config)] = exportedId
    }

    yield `// @ts-ignore\nexport default /*#__PURE__*/ { ${Object.entries(_default)
      .map(([key, id]) => `${JSON.stringify(key)}: ${id}`)
      .join(', ')} }`
  },
  mixed: {
    invalidateOnChanged: true,
    *load(files, config) {
      const acornOptions = Object.assign({ sourceType: 'module' }, config.acorn)
      const used_ids = new Set<string>()
      for (const file of files) {
        const namedExports: string[] = []
        const filename = getAbsModulePath(file)

        let imported = false

        let astBody: any
        try {
          astBody = (
            (this.getModuleInfo(filename)?.ast ??
              this.parse(fs.readFileSync(filename).toString(), acornOptions)) as any
          ).body
        } catch {
          const exportedId = uniqueString(config.moduleIdRenamer(filename, config), used_ids)
          yield `// @ts-ignore\nexport { default as ${exportedId} } from ${JSON.stringify(
            filename
          )};`
          continue
        }
        if (astBody) {
          try {
            for (const node of astBody) {
              const { type } = node
              if (type === 'ExportAllDeclaration') {
                let from = node.source.value
                if (from.startsWith('.')) {
                  from = nodePath.join(nodePath.dirname(filename), from)
                }
                yield `// @ts-ignore\nexport * from '${from}';`

                imported = true
              } else if (type === 'ExportDefaultDeclaration') {
                const exportedId = uniqueString(config.moduleIdRenamer(filename, config), used_ids)
                if (exportedId) {
                  yield `// @ts-ignore\nexport { default as ${exportedId} } from ${JSON.stringify(
                    filename
                  )};`
                  imported = true
                }
              } else if (type === 'ExportNamedDeclaration') {
                for (const specifier of node.specifiers) {
                  namedExports.push(specifier.exportedId.name)
                }
                if (node.declaration) {
                  for (const declaration of node.declaration.declarations || [node.declaration]) {
                    namedExports.push(declaration.id.name)
                  }
                }
              }
            }
          } catch {}

          const nameMapping = namedExports.map((namedExported) => {
            const name = uniqueString(
              config.moduleNamedExportRenamer(filename, namedExported, config),
              used_ids
            )
            return namedExported === name ? namedExported : `${namedExported} as ${name}`
          })
          if (nameMapping.length > 0) {
            yield `// @ts-ignore\nexport {${nameMapping.join(', ')}} from ${JSON.stringify(
              filename
            )};`
            imported = true
          }
          if (!imported) {
            yield `// @ts-ignore\nimport ${JSON.stringify(filename)};`
          }
        }
      }
    },
  },
  get 'vue-routes'(): Loader {
    let routeFileNameMap: Record<string, VueRouteRaw> = Object.create(null)
    let _config: BaseResolvedGlobConfig
    let _virtualPrefix = getUniqueIdentifier('page-')

    const prepareRoutes = (routes: VueRouteRaw[], parent?: any) => {
      let i = 0

      for (const route of routes) {
        if (parent) route.path = route.path.replace(/^\//, '')
        if (route.children) {
          route.children = prepareRoutes(route.children, route)
        }
        route.props = true
        Object.assign(route, _config.extendRoute?.(route, parent) || {})
        if (route[ImportModeKey] == null) {
          if (typeof _config.importMode === 'function') {
            route[ImportModeKey] = _config.importMode(route, parent)
          } else if (!_config.importMode || _config.importMode === 'auto') {
            let mode: string
            if (route.path === '/' || isCatchAllRoute(route[RawRouteKey])) {
              mode = 'sync'
            } else if (route.children?.length) {
              mode = makeLegalIdentifier(route[RawRouteKey])
              for (const c of route.children) {
                c[ImportModeKey] = mode
              }
            } else {
              mode = String(Math.floor(++i / 6))
            }
            route[ImportModeKey] = mode
          } else {
            route[ImportModeKey] = String(_config.importMode)
          }
        }
      }
      return routes
    }

    const stringifyRoutes = (routes: VueRouteRaw[]) => {
      const imports = new Set<string>()
      const nameSpaces: Record<string, Map<string, string>> = Object.create(null)
      let i = 0

      const stringify = createAnyValueStringify(function (key, value) {
        if (key === 'component' && typeof value === 'string') {
          const importMode: string = this[ImportModeKey]
          if (importMode === 'async') {
            return `// @ts-ignore\n() => import(${JSON.stringify(value)})`
          } else if (!importMode || importMode === 'sync') {
            const importName = getHashedIdentifier(value) + ++i
            const importStr = `// @ts-ignore\nimport ${importName} from ${JSON.stringify(value)};`

            // Only add import to array if it hasn't beed added before.
            imports.add(importStr)
            return importName
          } else {
            const importName = getHashedIdentifier(value) + ++i
            const id = _virtualPrefix + importMode
            ;(nameSpaces[id] ?? (nameSpaces[id] = new Map())).set(value, importName)
            return `// @ts-ignore\nasync () => (await import(${JSON.stringify(id)})).${importName}`
          }
        }
      })

      const stringRoutes = stringify(routes)

      for (const key in nameSpaces) {
        const values = nameSpaces[key]
        const code = [...values]
          .map(([fileName, importName]) => {
            _config.resolvedOptions.registerVirtualModule(
              key + importName,
              `// @ts-ignore\nexport { default } from ${JSON.stringify(fileName)};`
            )

            return `// @ts-ignore\nexport * as ${importName} from ${JSON.stringify(
              key + importName
            )};`
          })
          .join('\n')
        _config.resolvedOptions.registerVirtualModule(key, code)
      }

      return `${[...imports].join(
        '\n'
      )}\n// @ts-ignore\nexport default /*#__PURE__*/ ${stringRoutes};`
    }

    return {
      loadByImporter(importer, config) {
        _config = config
        let code = `// @ts-ignore\nexport { default } from ${JSON.stringify(config.id)};`
        importer = path.normalize(importer.replace(/[\?#].+$/, ''))
        let currentRoute = routeFileNameMap[importer]
        if (currentRoute && typeof currentRoute === 'object') {
          // @ts-ignore
          currentRoute = Object.fromEntries(
            Object.entries(currentRoute).map(([key, val]) => [makeLegalIdentifier(key), val])
          )
          delete currentRoute.current
          const keys = Object.keys(currentRoute)
          for (const key of keys) {
            code += `// @ts-ignore\nexport const ${key} = /*#__PURE__*/ ${stringify(
              currentRoute[key]
            )};`
          }
          code += `// @ts-ignore\nexport const current = /*#__PURE__*/ {${keys.join(',')}}`
        }
        return code
      },
      async load(files, config) {
        const used_names = new Set<string>()
        _config = config
        if (files.length === 0) {
          routeFileNameMap = Object.create(null)
          return `// @ts-ignore\nexport default /*#__PURE__*/ [];`
        }
        const base = normalizePath(path.posix.join('./', config.base ?? ''))
        const { globState, nuxtStyle, onRoutesGenerated } = config

        const routeName =
          typeof config.routeName === 'function'
            ? config.routeName
            : (p: string) => defaultRenamer(p, config)

        const prefix = path.resolve(CWD, globState.base)
        const suffix = urlSuffix(config.hash, config.query)

        const _routeFileNameMap: Record<string, VueRouteRaw> = Object.create(null)
        const routes: VueRouteRaw[] = []
        const _pageRoutes = files
          .map((file) => {
            const filename = getAbsModulePath(file)
            return [
              removeExtension(
                normalizePath(path.posix.join(base, path.relative(prefix, filename)))
              ),
              filename,
              uniqueString(routeName(filename), used_names),
            ] as const
          })
          .sort(([a], [b]) => {
            if (countSlash(a) === countSlash(b)) {
              const aDynamic = a.split('/').some((r) => isDynamicRoute(r, nuxtStyle))
              const bDynamic = b.split('/').some((r) => isDynamicRoute(r, nuxtStyle))
              if (aDynamic === bDynamic) return a.localeCompare(b)
              else return aDynamic ? 1 : -1
            } else {
              return countSlash(a) - countSlash(b)
            }
          })

        _pageRoutes.forEach(([routePath, fileName, name]) => {
          const pathNodes = routePath.split('/')

          const route: VueRouteRaw = (_routeFileNameMap[fileName] = {
            name,
            path: '',
            component: fileName + suffix,
            [RawRouteKey]: routePath,
            [RawComponentKey]: fileName,
          })

          let parentRoutes = routes

          for (let i = 0; i < pathNodes.length; i++) {
            const node = pathNodes[i]
            const isDynamic = isDynamicRoute(node, nuxtStyle)
            const isCatchAll = isCatchAllRoute(node, nuxtStyle)
            const normalizedName = isDynamic
              ? nuxtStyle
                ? isCatchAll
                  ? 'all'
                  : node.replace(/^_/, '')
                : node.replace(/^\[(\.{3})?/, '').replace(/\]$/, '')
              : node
            const normalizedPath = normalizedName.toLowerCase()

            // Check parent exits
            const parent = parentRoutes.find((parent) => {
              return route[RawRouteKey].startsWith(parent[RawRouteKey] + '/')
            })

            if (parent) {
              // Make sure children exits in parent
              parent.children = parent.children || []
              // Append to parent's children
              parentRoutes = parent.children
              // Reset path
              route.path = ''
            } else if (normalizedName.toLowerCase() === 'index') {
              if (!route.path) route.path = '/'
            } else if (normalizedName.toLowerCase() !== 'index') {
              if (isDynamic) {
                route.path += `/:${normalizedName}`
                // Catch-all route
                if (isCatchAll) route.path += '(.*)*'
              } else {
                route.path += `/${normalizedPath}`
              }
            }
          }

          parentRoutes.push(route)
        })

        // sort by dynamic routes
        let finalRoutes = prepareRoutes(routes, undefined)

        const allRoutes = finalRoutes.filter((route) => {
          return isCatchAllRoute(path.posix.parse(route[RawRouteKey]).name, nuxtStyle)
        })
        if (allRoutes.length > 1) {
          const [allRoute, ..._allRoutes] = allRoutes
          for (const route of _allRoutes) {
            delete routeFileNameMap[route[RawComponentKey]]
          }
          finalRoutes = finalRoutes.filter((route) => !allRoutes.includes(route))
          finalRoutes.push(allRoute)
        }

        finalRoutes = (await onRoutesGenerated?.(finalRoutes)) || finalRoutes
        routeFileNameMap = _routeFileNameMap
        return stringifyRoutes(finalRoutes)
      },
    }
  },
  get 'react-routes'(): Loader {
    let routeFileNameMap: Record<string, ReactRouteRaw> = Object.create(null)
    let _config: BaseResolvedGlobConfig
    let _virtualPrefix = getUniqueIdentifier('page-')

    const prepareRoutes = (routes: ReactRouteRaw[], parent?: any) => {
      let i = 0

      for (const route of routes) {
        if (parent) route.path = route.path.replace(/^\//, '')
        if (route.children) {
          route.children = prepareRoutes(route.children, route)
        }
        // @ts-ignore
        if (route.index) delete route.path

        Object.assign(route, _config.extendRoute?.(route, parent) || {})
        if (route[ImportModeKey] == null) {
          if (typeof _config.importMode === 'function') {
            route[ImportModeKey] = _config.importMode(route, parent)
          } else if (!_config.importMode || _config.importMode === 'auto') {
            let mode: string
            if (route.path === '/' || isCatchAllRoute(route[RawRouteKey])) {
              mode = 'sync'
            } else if (route.children?.length) {
              mode = makeLegalIdentifier(route[RawRouteKey])
              for (const c of route.children) {
                c[ImportModeKey] = mode
              }
            } else {
              mode = String(Math.floor(++i / 6))
            }
            route[ImportModeKey] = mode
          } else {
            route[ImportModeKey] = String(_config.importMode)
          }
        }
      }
      return routes
    }

    const stringifyRoutes = (routes: ReactRouteRaw[]) => {
      const imports = new Set<string>()
      imports.add(`// @ts-ignore\nimport React from "react";`)

      const nameSpaces: Record<string, Map<string, string>> = Object.create(null)
      let i = 0

      const stringify = createAnyValueStringify(function (key, value) {
        if (key === 'element' && typeof value === 'string') {
          const importMode: string = this[ImportModeKey]
          if (importMode === 'async') {
            return `// @ts-ignore\nReact.lazy(() => import(${JSON.stringify(value)}))`
          } else if (!importMode || importMode === 'sync') {
            const importName = getHashedIdentifier(value) + ++i
            const importStr = `// @ts-ignore\nimport ${importName} from ${JSON.stringify(value)};`

            // Only add import to array if it hasn't beed added before.
            imports.add(importStr)
            return `// @ts-ignore\nReact.createElement(${importName})`
          } else {
            const importName = getHashedIdentifier(value) + ++i
            const id = _virtualPrefix + importMode
            ;(nameSpaces[id] ?? (nameSpaces[id] = new Map())).set(value, importName)
            return `// @ts-ignore\nReact.lazy(async () => (await import(${JSON.stringify(
              id
            )})).${importName})`
          }
        }
      })

      const stringRoutes = stringify(routes)

      for (const key in nameSpaces) {
        const values = nameSpaces[key]
        const code = [...values]
          .map(([fileName, importName]) => {
            _config.resolvedOptions.registerVirtualModule(
              key + importName,
              `// @ts-ignore\nexport { default } from ${JSON.stringify(fileName)};`
            )

            return `// @ts-ignore\nexport * as ${importName} from ${JSON.stringify(
              key + importName
            )};`
          })
          .join('\n')
        _config.resolvedOptions.registerVirtualModule(key, code)
      }

      return `${[...imports].join(
        '\n'
      )}\n// @ts-ignore\nexport default /*#__PURE__*/ ${stringRoutes};`
    }

    return {
      loadByImporter(importer, config) {
        _config = config
        let code = `// @ts-ignore\nexport { default } from ${JSON.stringify(config.id)};`
        importer = path.normalize(importer.replace(/[\?#].+$/, ''))
        let currentRoute = routeFileNameMap[importer]
        if (currentRoute && typeof currentRoute === 'object') {
          // @ts-ignore
          currentRoute = Object.fromEntries(
            Object.entries(currentRoute).map(([key, val]) => [makeLegalIdentifier(key), val])
          )
          delete currentRoute.current
          const keys = Object.keys(currentRoute)
          for (const key of keys) {
            code += `// @ts-ignore\nexport const ${key} = ${stringify(currentRoute[key])};`
          }
          code += `// @ts-ignore\nexport const current = {${keys.join(',')}}`
        }
        return code
      },
      async load(files, config) {
        _config = config
        if (files.length === 0) {
          routeFileNameMap = Object.create(null)
          return `// @ts-ignore\nexport default /*#__PURE__*/ [];`
        }
        const base = normalizePath(path.posix.join('./', config.base ?? ''))
        const { globState, nuxtStyle, onRoutesGenerated } = config
        const prefix = path.resolve(CWD, globState.base)
        const suffix = urlSuffix(config.hash, config.query)
        const _routeFileNameMap: Record<string, ReactRouteRaw> = Object.create(null)
        const routes: ReactRouteRaw[] = []
        const _pageRoutes = files
          .map((file) => {
            const filename = getAbsModulePath(file)
            return [
              removeExtension(
                normalizePath(path.posix.join(base, path.relative(prefix, filename)))
              ),
              filename,
            ] as const
          })
          .sort(([a], [b]) => {
            if (countSlash(a) === countSlash(b)) {
              const aDynamic = a.split('/').some((r) => isDynamicRoute(r, nuxtStyle))
              const bDynamic = b.split('/').some((r) => isDynamicRoute(r, nuxtStyle))
              if (aDynamic === bDynamic) return a.localeCompare(b)
              else return aDynamic ? 1 : -1
            } else {
              return countSlash(a) - countSlash(b)
            }
          })

        _pageRoutes.forEach(([routePath, fileName]) => {
          const pathNodes = routePath.split('/')

          let parentRoutes = routes

          for (let i = 0; i < pathNodes.length; i++) {
            const node = pathNodes[i]
            const isDynamic = isDynamicRoute(node, nuxtStyle)
            const isCatchAll = isCatchAllRoute(node, nuxtStyle)
            const normalizedName = isDynamic
              ? nuxtStyle
                ? isCatchAll
                  ? 'all'
                  : node.replace(/^_/, '')
                : node.replace(/^\[(\.{3})?/, '').replace(/\]$/, '')
              : node
            const normalizedPath = normalizedName.toLowerCase()

            const route: ReactRouteRaw = (_routeFileNameMap[fileName] = {
              path: '',
              [RawRouteKey]: pathNodes.slice(0, i + 1).join('/'),
              [RawComponentKey]: fileName,
            })

            if (i === pathNodes.length - 1) {
              route.element = fileName + suffix
            }

            if (!route.path && normalizedPath === 'index') {
              route.index = true
            } else if (normalizedPath !== 'index') {
              if (isDynamic) {
                route.path = `:${normalizedName}`
                // Catch-all route
                if (isCatchAll) route.path = '*'
              } else {
                route.path = `${normalizedPath}`
              }
            }

            // Check parent exits
            const parent = parentRoutes.find((parent) => {
              return pathNodes.slice(0, i).join('/') === parent[RawRouteKey]
            })

            if (parent) {
              // Make sure children exits in parent
              parent.children = parent.children || []
              // Append to parent's children
              parentRoutes = parent.children
            }

            const exits = parentRoutes.some((parent) => {
              return pathNodes.slice(0, i + 1).join('/') === parent[RawRouteKey]
            })
            if (!exits) parentRoutes.push(route)
          }
        })

        // sort by dynamic routes
        let finalRoutes = prepareRoutes(routes, undefined)

        const allRoutes = finalRoutes.filter((route) => {
          return (
            route.element && isCatchAllRoute(path.posix.parse(route[RawRouteKey]).name, nuxtStyle)
          )
        })
        if (allRoutes.length > 1) {
          const [allRoute, ..._allRoutes] = allRoutes
          for (const route of _allRoutes) {
            delete routeFileNameMap[route[RawComponentKey]]
          }
          finalRoutes = finalRoutes.filter((route) => !allRoutes.includes(route))
          finalRoutes.push(allRoute)
        }

        finalRoutes = (await onRoutesGenerated?.(finalRoutes)) || finalRoutes
        routeFileNameMap = _routeFileNameMap
        return stringifyRoutes(finalRoutes)
      },
    }
  },
}

Object.setPrototypeOf(defaultloaders, null)

const defaultRenamer = (filename: string, config: BaseResolvedGlobConfig) => {
  const match = filename.match(config.globRegExp)
  if (match) {
    const matches = Array.from(match)
    matches.shift()

    let last = matches.pop()
    if (last) {
      for (const ext of config.resolvedOptions.extensions) {
        if (
          last === ext ||
          last === 'index' + ext ||
          last === '/index' + ext ||
          last === '\\index' + ext
        ) {
          last = ''
          return
        }
      }
    }

    matches.push(last!)

    const result = matches.filter(Boolean).join('_')
    if (result) return result
  }

  return normalizePath(path.relative(CWD, filename))
}
const defaultNamedExportRenamer = (filename: string, name: string) => {
  return name
}

let i = 0

export const resolveOptions = (options: UserOptions = {}) => {
  let { defaultLoader = 'default', exclude = [], extensions: _extensions = extensions } = options

  exclude = toArray(exclude)

  const virtualModules: Record<string, string> = Object.create(null)

  const registerVirtualModule = (id: string, code: string) => {
    virtualModules[id] = code
  }

  const resolveVirtualModuleId = (id: string) => {
    return virtualModules[id] != null ? id : undefined
  }

  const loadVirtualModule = (id: string) => {
    return virtualModules[id] != null ? virtualModules[id] : undefined
  }

  const pluginId = 'plugin-' + ++i

  const globConfigs: Record<string, BaseResolvedGlobConfig> = Object.create(Object.create(null))

  return {
    defaultLoader,
    exclude,
    registerVirtualModule,
    resolveVirtualModuleId,
    loadVirtualModule,
    pluginId,
    extensions: _extensions.map((ext) => (ext.startsWith('.') ? ext : '.' + ext)).filter(Boolean),
    globConfigs,
  }
}

export type ResolvedOptions = ReturnType<typeof resolveOptions>

export const parseGlobConfig = (_glob: GlobConfig, resolvedOptions: ResolvedOptions) => {
  let {
    glob,
    aliases: _aliases,
    loader: _loader,
    exclude: _exclude = [],
    // @ts-ignore
    dynamicImport: _dynamicImport,
    // @ts-ignore
    moduleKeyRenamer,
    // @ts-ignore
    moduleIdRenamer,
    // @ts-ignore
    moduleNamedExportRenamer,
    transform: _transform,

    ...others
  } = _glob

  if (/..+:|\0/.test(glob)) return

  glob = glob.includes(':')
    ? glob
    : path.posix.join(normalizePath(glob.startsWith('/') ? path.resolve('/') : CWD), glob)

  const __aliases = new Set(toArray<string>(_aliases))
  if (typeof _loader === 'string') {
    __aliases.add(`glob-${_loader}:${glob}`)
  }

  let match, hash, query

  if ((match = glob.match(/^(?<glob>.+?)\?(?!\()(?<query>.+?)$/))) {
    ;({ glob, query } = match.groups!)
  }
  ;[glob, hash] = glob.split('#', 2)
  const loader =
    (typeof _loader === 'string' ? defaultloaders[_loader] : _loader) ||
    (typeof resolvedOptions.defaultLoader === 'string'
      ? defaultloaders[resolvedOptions.defaultLoader]
      : resolvedOptions.defaultLoader) ||
    defaultloaders.default

  let globState = picomatch.scan(glob)

  if (!/(\.[^\/\\\*\.]*$)|(\*\*$)/.test(globState.glob)) {
    glob = globState.base + globState.glob
    if (globState.glob.endsWith('/')) {
      glob += `index?(${resolvedOptions.extensions.join(',')})`
    } else {
      glob += `?(${[
        ...resolvedOptions.extensions,
        '/index',
        ...resolvedOptions.extensions.map((e) => '/index' + e),
      ].join(',')})`
    }
  }

  if (typeof _loader === 'string') {
    __aliases.add(`glob-${_loader}:${glob + urlSuffix(hash, query)}`)
  }

  globState = picomatch.scan(glob)
  const globRegExp = picomatch.makeRe(glob, { capture: true }, false, false)

  const _moduleKeyRenamer =
    typeof moduleKeyRenamer === 'function' ? moduleKeyRenamer : defaultRenamer
  moduleKeyRenamer = (...args: any[]) => String(_moduleKeyRenamer(...args))

  const _moduleIdRenamer = typeof moduleIdRenamer === 'function' ? moduleIdRenamer : defaultRenamer
  moduleIdRenamer = (...args: any[]) => makeLegalIdentifier(String(_moduleIdRenamer(...args)))

  const _moduleNamedExportRenamer =
    typeof moduleNamedExportRenamer === 'function'
      ? moduleNamedExportRenamer
      : defaultNamedExportRenamer
  moduleNamedExportRenamer = (...args: any[]) =>
    makeLegalIdentifier(String(_moduleNamedExportRenamer(...args)))

  const exclude = [...toArray(_exclude), ...resolvedOptions.exclude]

  let dynamicImport = typeof _dynamicImport === 'function' ? _dynamicImport : () => _dynamicImport

  // @ts-ignore
  const transform = mergeTransformHook(loader.transform, _transform)

  const aliases = Array.from(__aliases)

  let watcher: Promise<GlobImportWatch>

  const result: BaseResolvedGlobConfig = {
    // @ts-ignore
    load: loader,
    ...others,
    ...loader,
    aliases,
    moduleIdRenamer,
    moduleKeyRenamer,
    moduleNamedExportRenamer,
    glob,
    globState,
    globRegExp,
    dynamicImport,
    exclude,
    transform,
    query,
    hash,
    async start(context) {
      await (watcher ?? (watcher = globImportWatch(context, result, async (code) => {
        await outputFile(id, code)
      })))
    },
    generateCode(context) {
      return new Promise<string>(resolve => {
        globImportWatch(context as any,result, resolve)
      })
    },
    close() {
      watcher?.then((watcher) => watcher.close())
    },
    resolvedOptions,
  }

  const id = path.resolve(
    'node_modules/.rollup-plugin-glob-import',
    getHashedIdentifier(
      resolvedOptions.pluginId + result.load.name + hash + query + glob + aliases.join(',') + CWD,
      '_glob-' + result.load.name + '-',
      16
    ) + '.tsx'
  )

  result.id = id

  return result
}

const MODULE_ID_RE = /^glob(?:-(?<loader>[^:]+)):(?<glob>.+)$/

export const resolveGlob = (
  glob: GlobConfig | string,
  resolvedOptions: ResolvedOptions,
  importer?: string | undefined
) => {
  if (typeof glob === 'string') {
    const resolvedGlob = resolvedOptions.globConfigs[glob]
    if (resolvedGlob) return resolvedGlob

    let match, defaultLoader
    if ((match = glob.match(MODULE_ID_RE))) {
      ;({ glob, loader: defaultLoader } = match.groups!)
    } else {
      return
    }
    defaultLoader = defaultLoader || (resolvedOptions.defaultLoader as any)

    if (!isGlob(glob) || /..+:|\0/.test(glob)) return

    if (importer && !path.isAbsolute(glob)) {
      if (/^\.[\/\\]/.test(glob)) {
        glob = path.posix.join(normalizePath(path.relative(CWD, path.dirname(importer))), glob)
      } else {
        try {
          const p = path.dirname(
            require.resolve(path.posix.join(glob.split(/[\/\\]/)[0], 'package.json'))
          )
          glob = path.posix.join(
            normalizePath(path.relative(CWD, p)),
            glob.split(/[\/\\]/)[1] ?? '.'
          )
        } catch (error) {}
      }
    }

    if (typeof defaultLoader === 'string') {
      const resolvedGlob = resolvedOptions.globConfigs[`glob-${defaultLoader}:${glob}`]
      if (resolvedGlob) return resolvedGlob
    }

    glob = {
      glob,
      loader: defaultLoader,
      aliases: [
        typeof defaultLoader === 'string' && `glob-${defaultLoader}:${glob}`,
        defaultLoader === resolvedOptions.defaultLoader && `glob:${glob}`,
      ].filter(Boolean) as string[],
    }
  }
  const resolvedGlob = parseGlobConfig(glob, resolvedOptions)
  if (resolvedGlob) {
    for (const k of resolvedGlob.aliases) {
      Object.getPrototypeOf(resolvedOptions.globConfigs)[k] = resolvedGlob
    }
    resolvedOptions.globConfigs[resolvedGlob.id] = resolvedGlob
  }

  return resolvedGlob
}
